基于“贫血”模型的传统开发模式是否违背 OOP
王争
《数据结构与算法之美》作者
读完需要
7分钟速读仅需 3 分钟
据作者了解,大部分工程师是做业务开发的,很多业务系统都是基于 MVC 三层架构开发的。实际上,更确切地讲,这是一种基于“贫血”模型的 MVC 三层架构开发模式。虽然这种开发模式已经成为标准的 Web 项目的开发模式,但它违反了面向对象编程风格,是彻彻底底的面向过程编程风格,因此,被有些人称为反模式(anti-pattern)。特别是领域驱动设计(Domain Driven Design,DDD)流行之后,这种基于“贫血”模型的传统开发模式开始被人诟病。而基于“充血”模型的 DDD 开发模式开始被人提倡。在本节中,我们介绍这两种开发模式,并探讨下列问题:
为什么基于“贫血”模型的传统开发模式违反 OOP?
基于“贫血”模型的传统开发模式既然违反 OOP,那么为什么如此流行?
我们应该在什么情况下考虑使用基于“充血”模型的 DDD 开发模式?
1
基于“贫血”模型的传统开发模式
作者相信,大部分后端开发工程师不会对 MVC 三层架构感到陌生。不过,为了统一大家对 MVC 的认识,作者在 2.5.3 节介绍的基础上,扩展介绍一下 MVC 三层架构。
MVC 将整个项目分为 3 层:展示层、逻辑层和数据层。MVC 三层架构是一种笼统的分层方式,落实到具体的开发层面,很多项目并不会完全遵从 MVC 固定的分层方式,而是会根据具体的项目需求,进行适当调整。
例如,目前,很多 Web 都是前后端分离的,后端负责暴露接口供前端调用。在这种情况下,我们一般将后端项目分为 3 层:Repository、Service 和 Controller。其中,Repository 层负 责数据访问,Service 层负责业务逻辑,Controller 层负责暴露接口。当然,这只是其中一种分层和命名方式。尽管不同的团队会针对不同的项目进行调整,但基本的分层思路类似。
在介绍完 MVC 三层架构之后,我们介绍什么是“贫血”模型。
实际上,读者可能一直在使用“贫血”模型进行开发,只是自己不知道而已。毫不夸张地讲,据作者了解,目前几乎所有的业务后端系统都是基于“贫血”模型开发的。我们举例解释一下,代码如下所示:
/** Controller+VO(View Object) **/
public class UserController {
// 通过构造函数或 IoC(控制反转)框架注入
private UserService userService;
public UserVo getUserById(Long userId) {
UserBo userBo = userService.getUserById(userId);
UserVo userVo = [...convert userBo to userVo...];
return userVo;
}
}
public class UserVo { //省略其他属性、getter/setter/constructor方法
private Long id;
private String name;
private String cellphone;
}
/**Service+BO(Business Object) **/
public class UserService {
private UserRepository userRepository; //通过构造函数或IoC框架注入
public UserBo getUserById(Long userId) {
UserEntity userEntity = userRepository.getUserById(userId);
UserBo userBo = [...convert userEntity to userBo...];
return userBo;
}
}
public class UserBo { //省略其他属性、getter/setter/constructor方法
private Long id;
private String name;
private String cellphone;
}
/**Repository+Entity **/
public class UserRepository {
public UserEntity getUserById(Long userId) { //... }
}
public class UserEntity { //省略其他属性、getter/setter/constructor方法
private Long id;
private String name;
private String cellphone;
}
实际上,在平时开发 Web 后端项目时,我们基本上都是像上述代码那样组织代码的。其中,UserEntity 类和 UserRepository 类组成了数据访问层,UserBo 类和 UserService 类组成了业务逻辑层,UserVo 类和 UserController 类在这里属于接口层。
从上述代码中,我们可以发现,UserBo 类是一个纯粹的数据结构,只包含数据,不包含任何业务逻辑。业务逻辑集中在 UserService 类中。我们通过 UserService 类操作 UserBo 类。换句话说,Service 层的数据和业务逻辑被分割到两个类中。像 UserBo 这样只包含数据,不包含业务逻辑的类,称为“贫血”模型(Anemic Domain Model)。同理,UserEntity 类和 UserVo 类都是基于“贫血”模型设计的。“贫血”模型将数据与操作分离,破坏了面向对象编程的封装特性,属于典型的面向过程编程风格。
2
基于“充血”模型的 DDD 开发模式
上面讲了基于“贫血”模型的传统开发模式,接下来,我们再来看一下基于“充血”模型的DDD 开发模式。
首先,我们介绍一下什么是“充血”模型
在“贫血”模型中,数据和业务逻辑被分割到不同的类中。“充血”模型(Rich Domain Model)正好相反,数据和对应的业务逻辑被封装到同一个类中。因此,“充血”模型满足面向对象编程的封装特性,属于典型的面向对象编程风格。
然后,我们介绍一下什么是领域驱动设计
领域驱动设计(DDD)主要用来指导如何解耦业务系统,划分业务模块,以及定义业务领域模型及其交互。
领域驱动设计这个概念并不新颖,早在 2004 年就被提出,发展到现在,已经有十几年的历史了。不过,它被大众熟知,还是因为微服务的兴起。
我们知道,除监控、调用链追踪和 API 网关等服务治理系统的开发以外,微服务还有一个更加重要的工作,那就是对公司的业务合理地进行服务划分。而领域驱动设计恰好是用来指导服务划分的。因此,微服务加速了领域驱动设计的流行。
不过,作者认为,领域驱动设计类似敏捷开发、SOA 和 PaaS 等,这些概念听起来“高大上”,实际上没有太多复杂的内容。
即便读者对领域驱动设计这个概念一无所知,只要读者开发过业务系统,就会或多或少用过它。做好领域驱动设计的关键是对业务的熟悉程度,而并不是对领域驱动设计这个概念本身的理解程度。即便我们非常清楚领域驱动设计这个概念,但是,如果我们对业务不熟悉,那么也不能得到合理的领域设计。
因此,我们不要把领域驱动设计当成“银弹”(可以简单地理解为“万金油”),没必要花太多的时间过度地研究它。
实际上,基于“充血”模型的 DDD 开发模式实现的代码一般是按照 MVC 三层架构分层的。Controller 层还是负责暴露接口,Repository 层还是负责数据访问,Service 层负责业务逻辑。它与基于“贫血”模型的传统开发模式的主要区别在 Service 层。
在基于“贫血”模型的传统开发模式中,Service 层包含 Service 类和 BO 类两部分,BO 类是“贫血”模型,只包含数据,不包含具体的业务逻辑。业务逻辑集中在 Service 类中。
在基于“充血”模型的 DDD 开发模式中,Service 层包含 Service 类和 Domain 类两部分。Domain 类相当于“贫血”模型中的 BO 类。与 BO 类的区别在于,Domain 类是基于“充血”模型开发的,既包含数据,又包含业务逻辑。而 Service 类变得非常“单薄”。
总结一下,基于“贫血”模型的传统的开发模式,重 Service 类,轻 BO 类;基于“充血”模型的 DDD 开发模式, 轻 Service 类,重 Domain 类。
3
两种开发模式的应用对比
我们通过一个稍微复杂的例子介绍如何应用这两种开发模式进行开发,特别是基于“充血”模型的 DDD 开发模式。很多具有购买、支付功能的应用(如淘宝、京东金融等)都支持“钱包”功能。应用为每个用户开设一个系统内的虚拟钱包账户。虚拟钱包的基本操作大致包含入账、出账、转账和查询余额等。我们开发一个接口系统来供前端或其他系统调用,实现入账、出账、转账和查询余额等基本操作。
我们先看一下如何利用基于“贫血”模型的传统开发模式开发这个系统。我们还是应用经典的 MVC 三层结构。其中,Controller 和 VO 负责暴露接口,具体的代码结构如下所示。注意,在 Controller 中,接口实现比较简单,主要是调用 Service 方法,因此,代码中省略了这部分实现。(未完待续)
本文节选自王争《设计模式之美》
往期推荐